Skip to content

Do not create conditional expression when guard type overlaps with non-object type in other branch#5466

Closed
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-ezv172c
Closed

Do not create conditional expression when guard type overlaps with non-object type in other branch#5466
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-ezv172c

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

Fixes a false positive "Negated boolean expression is always false" when a variable is assigned from an array access in an elseif branch and from an independent expression in the if branch, and the variable's truthiness is later checked.

Changes

  • Added a new skip condition in MutatingScope::createConditionalExpressions() (src/Analyser/MutatingScope.php) that prevents creating a conditional expression when:

    1. The target expression does not exist in the other branch's expression types
    2. The guard expression exists in the other branch with certainty
    3. The other branch's guard type is a non-object type (where isSuperTypeOf gives precise overlap results)
    4. The guard type overlaps with the other branch's guard type
  • Added regression test in tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php and tests/PHPStan/Rules/Comparison/data/bug-14469.php with the exact reproduction case and variants (strict comparison, different array key, ternary expression)

  • Added NSRT tests in tests/PHPStan/Analyser/nsrt/bug-14469.php and tests/PHPStan/Analyser/nsrt/bug-14469-variants.php to verify correct type inference for array accesses inside truthiness checks after if/elseif blocks, including variants with:

    • if/else with nested conditions
    • Multiple elseif branches
    • Nested array access

Root cause

When merging scopes from if/elseif branches, createConditionalExpressions() creates conditional expressions that link variable types across branches. For example, if $aa = $R['aa'] in the elseif branch (where $R['aa'] is truthy), a conditional expression "if $aa is truthy, then $R['aa'] is truthy" gets created.

This conditional expression is invalid when the if-branch assigns $aa from an independent source (e.g., $aa = $user->id === 10 ? 2 : null) — the value 2 is truthy but says nothing about $R['aa']. The existing skip condition (added for bug-14411) only checked when both the target and guard exist in the other branch. This fix extends it to also check when the target does NOT exist in the other branch but the guard type overlaps with the other branch's guard type.

The isObject()->no() guard is necessary because isSuperTypeOf gives imprecise "Maybe" results for object/interface types due to theoretical subclass relationships (e.g., Event->isSuperTypeOf(OrderInterface) returns Maybe even when the branches are semantically disjoint from instanceof narrowing). For non-object types (scalars, mixed, etc.), isSuperTypeOf gives precise overlap information.

Analogous cases probed

  • Other constant condition rules (IfConstantCondition, TernaryOperatorConstantCondition, etc.): These all consume the same scope type information, so the fix in MutatingScope applies to all of them.
  • Strict comparison (=== false): Tested and passes (variant in test data).
  • Ternary operator: Tested and passes (variant in test data).
  • Different array keys: Tested and passes (variant in test data).
  • Nested array access: Tested and passes (variant in NSRT test).
  • Multiple elseif branches: Tested and passes (variant in NSRT test).
  • if/else with nested conditions: Tested and passes (variant in NSRT test).
  • Object/interface types (bug-14411-regression case): Verified that the fix does NOT affect the regression test for bug-14411, which relies on conditional expressions for object types to track variable definedness.

Test

  • tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest::testBug14469 — verifies no false positive for the exact reported case and three variants
  • tests/PHPStan/Analyser/nsrt/bug-14469.php — verifies $R['aa'] has type mixed (not truthy) inside if ($aa) block
  • tests/PHPStan/Analyser/nsrt/bug-14469-variants.php — verifies correct type inference for three control flow variants (if/else, multiple elseif, nested array)

Fixes phpstan/phpstan#14469

…n-object type in other branch

- In `MutatingScope::createConditionalExpressions()`, add a second skip
  condition for cases where the target expression does not exist in the
  other branch but the guard type overlaps with the other branch's guard
  value (for non-object types where `isSuperTypeOf` gives precise answers)
- This prevents false positive "always true/false" reports when a variable
  is assigned from an array access in one branch and from an independent
  expression in another branch, and the shared variable's truthiness is
  later checked
- The existing skip condition (from bug-14411 fix) only handles the case
  where the target exists in both branches; this extends it to handle the
  case where the target only exists in one branch
- The `isObject()->no()` guard ensures we only apply this for non-object
  types where `isSuperTypeOf` overlap detection is reliable (object types
  can give imprecise Maybe results due to theoretical subclass overlap)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@staabm staabm deleted the create-pull-request/patch-ezv172c branch April 17, 2026 16:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants